package com.aabbc.database.config;

import com.aabbc.database.bean.MultiRoutingDataSource;
import com.zaxxer.hikari.HikariDataSource;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.beans.factory.support.GenericBeanDefinition;
import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
import org.springframework.boot.context.properties.source.ConfigurationPropertyNameAliases;
import org.springframework.boot.context.properties.source.ConfigurationPropertySource;
import org.springframework.boot.context.properties.source.MapConfigurationPropertySource;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.lang.NonNullApi;
import org.springframework.util.CollectionUtils;

import javax.sql.DataSource;
import java.util.*;

/**
 * MultiDataSourceRegister
 *
 * @author: chenfenghai
 * @version: 1.0
 * @date: 2020-04-02 15:28
 * @blame Team
 */
public class MultiDataSourceRegister implements EnvironmentAware, ImportBeanDefinitionRegistrar {

    private static final String DATASOURCE_CONFIG_PREFIX = "spring.datasource.";
    private static final String DATASOURCE_CONFIG_NAMES = DATASOURCE_CONFIG_PREFIX + "names";
    private static final String DATASOURCE_CONFIG_TYPE = "type";
    public static final List<String> DATASOURCE_NAME_LIST = new ArrayList<>(8);
    /** 别名 */
    private static final ConfigurationPropertyNameAliases aliases = new ConfigurationPropertyNameAliases();
    /** 数据源列表 */
    private Map<String, DataSource> dataSourceMap = new HashMap<>(8);
    /** 配置上下文（也可以理解为配置文件的获取工具） */
    private Environment evn;
    /** 参数绑定工具 */
    private Binder binder;

    static {
        //由于部分数据源配置不同，所以在此处添加别名，避免切换数据源出现某些参数无法注入的情况
        aliases.addAliases("url", "jdbc-url");
        aliases.addAliases("username", "user");
    }

    /**
     * Set the {@code Environment} that this component runs in.
     *
     * @param environment 环境配置
     */
    @Override
    public void setEnvironment(Environment environment) {
        this.evn = environment;
        // 绑定配置器
        binder = Binder.get(evn);
    }

    /**
     * Register bean definitions as necessary based on the given annotation metadata of
     * the importing {@code @Configuration} class.
     * <p>Note that {@link BeanDefinitionRegistryPostProcessor} types may <em>not</em> be
     * registered here, due to lifecycle constraints related to {@code @Configuration}
     * class processing.
     *
     * @param importingClassMetadata annotation metadata of the importing class
     * @param registry               current bean definition registry
     */
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        // 获取所有数据源名称
        String dataSourceNames = binder.bind(DATASOURCE_CONFIG_NAMES, String.class).get();
        Objects.requireNonNull(dataSourceNames);
        Arrays.asList(StringUtils.split(dataSourceNames, ",")).forEach((dataSourceName)->{
            Map dataSourceProperties = binder.bind(DATASOURCE_CONFIG_PREFIX + dataSourceName, Map.class).get();
            String typeClazz = Objects.toString(dataSourceProperties.get(DATASOURCE_CONFIG_TYPE), null);
            Class<? extends DataSource> dataSourceTypeClazz = getDataSourceType(typeClazz);
            DataSource dataSource = bind(dataSourceTypeClazz, dataSourceProperties);
            dataSourceMap.put(dataSourceName, dataSource);
            DATASOURCE_NAME_LIST.add(dataSourceName);
        });
        // bean定义类
        GenericBeanDefinition define = new GenericBeanDefinition();
        // 设置bean的类型，此处MultiDataSource是继承AbstractRoutingDataSource的实现类
        define.setBeanClass(MultiRoutingDataSource.class);
        // 需要注入的参数，类似spring配置文件中的<property/>
        MutablePropertyValues mpv = define.getPropertyValues();
        // 添加默认数据源，避免key不存在的情况没有数据源可用
        mpv.add("defaultTargetDataSource", dataSourceMap.values().iterator().next());
        // 添加其他数据源
        mpv.add("targetDataSources", dataSourceMap);
        // 将该bean注册为datasource，不使用springboot自动生成的datasource
        registry.registerBeanDefinition("multiRoutingDataSource", define);
    }

    /**
     * 通过字符串获取数据源class对象
     *
     * @param typeStr
     * @return
     */
    private Class<? extends DataSource> getDataSourceType(String typeStr) {
        try {
            // 字符串不为空则通过反射获取class对象
            // 默认为hikariCP数据源，与springboot默认数据源保持一致
            if (StringUtils.isNotEmpty(typeStr)) {
                return (Class<? extends DataSource>) Class.forName(typeStr);
            } else {
                return HikariDataSource.class;
            }
        } catch (Exception e) {
            // 无法通过反射获取class对象的情况则抛出异常
            throw new IllegalArgumentException("can not resolve class with type: " + typeStr);
        }
    }

    private <T extends DataSource> T bind(Class<T> clazz, Map properties) {
        ConfigurationPropertySource source = new MapConfigurationPropertySource(properties);
        Binder sourceBinder = new Binder(source.withAliases(aliases));
        // 通过类型绑定参数并获得实例对象
        return sourceBinder.bind(ConfigurationPropertyName.EMPTY, Bindable.of(clazz)).get();
    }

}
